import { BizFailedCodeEnum } from '@/common/biz-failed/biz-failed.enum';
import { BizFailedException } from '@/common/biz-failed/biz-failed.exception';
import { Injectable } from '@nestjs/common';
import {
  AdminCreateDto,
  AdminIndexDto,
  AdminLoginDto,
  AdminUpdateDto,
  AdminUpdateMinePasswordDto,
} from '@/module/admin/admin.dto';
import { MoreThan, Repository } from 'typeorm';
import { InjectRepository } from '@nestjs/typeorm';
import { AdminModel } from '@/module/admin/admin.model';
import { Redis } from 'ioredis';
import { RedisNamespace } from '@/common/config/config.interface';
import { RedisService } from '@liaoliaots/nestjs-redis';
import { AdminLoginLogModel } from '@/module/admin-login-log/admin-login-log.model';
import { AdminAuthGuard } from '@/common/auth/admin-auth.guard';
import { createHmac } from 'crypto';
import { nanoid } from 'nanoid';
import { ArticleStatus, BooleanNumber } from "common/utils/data";
import { omitUndefined } from "@/common/utils/utils";
import dayjs from "dayjs";
import { Transactional } from "typeorm-transactional";
import { AdminuserRoleModel } from "@/module/adminuser-role/adminuser-role.model";
import { RoleResourceModel } from "@/module/role-resource/role-resource.model";
import { AdminIndexResult } from "@/module/admin/admin.result";

@Injectable()
export class AdminService {
  adminLoginRedisServiceClient: Redis;
  constructor(
    private readonly redisService: RedisService,
    @InjectRepository(AdminModel)
    private readonly adminModelRepository: Repository<AdminModel>,
    @InjectRepository(AdminLoginLogModel)
    private readonly adminLoginLogModelRepository: Repository<AdminLoginLogModel>,
    @InjectRepository(AdminuserRoleModel)
    private readonly adminuserRoleModelRepository: Repository<AdminuserRoleModel>,
    @InjectRepository(RoleResourceModel)
    private readonly roleResourceModelRepository: Repository<RoleResourceModel>,
  ) {
    this.adminLoginRedisServiceClient = this.redisService.getClient(
      RedisNamespace.admin_login,
    );
  }

  async login(body: AdminLoginDto) {
    const admin = await this.adminModelRepository.findOne({
      where: {
        username: body.username,
        password: this.sha256Password(body.password), // 密码admin WDm23XtQD/BJZSwQrN3u+u0eKEE3YLp1STTt06cs7TA=
      }
    });
    if (!admin) {
      throw new BizFailedException(
        BizFailedCodeEnum.ADMIN_USERNAME_PASSWORD_WRONG,
      );
    }
    if (!admin.status) {
      throw new BizFailedException(BizFailedCodeEnum.ADMIN_DISABLED);
    }
    const token = nanoid();
    await Promise.all([
      this.adminLoginRedisServiceClient.set(
        token,
        JSON.stringify(admin),
        'EX',
        AdminAuthGuard.authExpireSecond,
      ),
      this.adminLoginLogModelRepository.save({
        token,
        adminUuid: admin.uuid,
      }),
    ]);
    return token;
  }

  async logout(token: string): Promise<void> {
    await this.adminLoginRedisServiceClient.del(token);
  }

  async adminMine(uuid: string) {
    const admin = await this.adminModelRepository.createQueryBuilder('admin')
      .leftJoinAndSelect('admin.adminuserRoles', 'adminuserRoles')
      .leftJoinAndSelect('adminuserRoles.role', 'role')
      .select([
        'admin.uuid',
        'admin.status',
        'admin.username',
        'admin.name',
        'admin.mobile',
        'admin.job',
        'admin.createTime',
        'admin.updateTime',
        'adminuserRoles.uuid',
        'role.uuid',
        'role.name',
      ])
      .andWhere(
        `(admin.uuid = :uuid)`,
        {
          uuid,
        },
      )
      .getOne();

    if(!admin){
      return null;
    }

    const roleUuids = admin.adminuserRoles.map(item=>item.role.uuid);

    const resources: string[] = [];
    if(roleUuids?.length){
      const roleResources = await this.roleResourceModelRepository.createQueryBuilder('roleResource')
        .leftJoinAndSelect('roleResource.resource', 'resource')
        .select([
          'roleResource.uuid',
          'resource.uuid',
          'resource.key'
        ])
        .andWhere(`(roleResource.roleUuid In (:...roleUuids))`, {
          roleUuids,
        })
        .getMany()
      roleResources.forEach(roleResource=>{
        resources.push(roleResource.resource.key);
      })
    }

    const {adminuserRoles, ...restAdmin} = admin

    return {
      ...restAdmin,
      roleNames: adminuserRoles.map(item=>item.role?.name),
      resources,
    }
  }

  async adminIndex(query: AdminIndexDto) {
    const queryBuilder = this.adminModelRepository.createQueryBuilder('admin')
      .leftJoinAndSelect('admin.adminuserRoles', 'adminuserRoles')
      .leftJoinAndSelect('adminuserRoles.role', 'role')
      .select([
        'admin.id',
        'admin.uuid',
        'admin.status',
        'admin.username',
        'admin.name',
        'admin.mobile',
        'admin.job',
        'admin.createTime',
        'admin.updateTime',
        'adminuserRoles.uuid',
        'role.uuid',
        'role.name',
      ])
      .limit(query.getPageSize())
      .offset(query.getOffset())
      .orderBy({
        'admin.id': 'DESC',
      });

    if (query.status) {
      queryBuilder.andWhere(
        `(admin.status = :status)`,
        {
          status: Number(query.status),
        },
      );
    }

    const [admins, count] = await queryBuilder.getManyAndCount();
    const res = admins.map(item=>{
      const {adminuserRoles, ...restAdmin} = item
      return {
        ...restAdmin,
        roleNames: adminuserRoles.map(item=>item.role?.name),
      }
    })
    return [res, count] as [AdminIndexResult[], number];
  }

  @Transactional()
  async adminCreate(body: AdminCreateDto): Promise<void> {
    try {
      const admin = await this.adminModelRepository.save({
        ...body,
        password: this.sha256Password(body.password),
      });
      await this.adminuserRoleModelRepository.save(body.roleUuids.map(item=>{
        return {
          adminUuid: admin.uuid,
          roleUuid: item,
        }
      }))
    } catch (e) {
      if (e.constraint === 'ADMIN.username') {
        throw new BizFailedException(BizFailedCodeEnum.ADMIN_USERNAME_EXISTS);
      }
      throw e;
    }
  }

  async adminUpdateMinePassword(body: AdminUpdateMinePasswordDto, uuid: string){
    const admin = await this.adminModelRepository.findOne({
      where: {
        uuid,
        password: this.sha256Password(body.oldPassword)
      }
    })
    if(!admin){
      throw new BizFailedException(BizFailedCodeEnum.ADMIN_PASSWORD_WRONG);
    }
    await this.adminUpdate({
      password: body.password,
    }, uuid);
  }

  @Transactional()
  async adminUpdate(body: AdminUpdateDto, uuid: string): Promise<void> {
    if (body.password) {
      body.password = this.sha256Password(body.password);
    }
    await this.adminModelRepository.update(
      {
        uuid,
      },
      {
        ...omitUndefined(body),
      },
    );
    if(body.roleUuids){
      await this.adminuserRoleModelRepository.delete({
        adminUuid: uuid,
      })
      await this.adminuserRoleModelRepository.save(body.roleUuids.map(item=>{
        return {
          adminUuid: uuid,
          roleUuid: item,
        }
      }))
    }
    if (body.status === BooleanNumber.false || body.password) {
      await this.kick(uuid);
    }
  }

  // 把该用户三个月内的token都踢下线
  private async kick(uuid: string) {
    const res = await this.adminLoginLogModelRepository.find({
      where: {
        adminUuid: uuid,
        createTime: MoreThan(dayjs().subtract(3, 'month').format('YYYY-MM-DD') as unknown as number),
      },
    });
    const delTokensPromises = res.map((item) => {
      return this.adminLoginRedisServiceClient.del(item.token);
    });
    await Promise.all(delTokensPromises);
  }

  private sha256Password(str: string): string {
    const hmac = createHmac('sha256', 'huihifi*&%&*(&%');
    hmac.update(str);
    return hmac.digest('base64');
  }
}
